/**
 * 
 */
package inz.model.patterns;

import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

/** Class representing database original sequence of elements. Contains additional structures that help in some
 * operations. Can be e.g. sequence of words in single document (for text sequence mining) */
public class DbSequence
{
	/** List of sequence elements with time of the element in this sequence */
	private final ArrayList<DbElement> elements = new ArrayList<DbElement>();
	/** alternative representation of sequence: element - list of times of occurrences of the element */
	// TODO zrobic template zapmiast stringa
	private final HashMap<String, ArrayList<Integer>> occurrences = new HashMap<String, ArrayList<Integer>>();

	// TODO przeniesc to gdzies
	private int windowSize = 0;
	/** counter of elements */
	private int elemCount = 0;

	public void addElement(String word)
	{
		// elements.add(new DbElement(word, elements.size() + 1));
		ArrayList<Integer> occurList = occurrences.get(word);
		if (occurList == null)
		{
			occurList = new ArrayList<Integer>();
			occurrences.put(word, occurList);
		}
		elemCount++;
		occurList.add(elemCount);
	}

	public Set<String> getElementSet()
	{
		return occurrences.keySet();
	}

	public int getLength()
	{
		Set<Integer> elems = new HashSet<Integer>();
		for (Map.Entry<String, ArrayList<Integer>> entry : occurrences.entrySet())
		{
			elems.addAll(entry.getValue());
		}
		return elems.size();
	}

	public int getSize()
	{
		int size = 0;
		for (Map.Entry<String, ArrayList<Integer>> entry : occurrences.entrySet())
		{
			size += entry.getValue().size();
		}
		return size;
	}

	/** generates 1-sequence set of all distinct elements of this database sequence
	 * @return set of 1-sequences from this database sequence */
	public Set<Sequence> get1SequenceSet()
	{
		Set<Sequence> resultSet = new HashSet<Sequence>();
		for (String item : occurrences.keySet())
		{
			resultSet.add(new Sequence(item));
		}
		return resultSet;
	}

	/** generates set of 2-sequence contained in this database sequence
	 * @param seedSet
	 * @return set of 2-sequences from this database sequence */
	public Set<Sequence> get2SequenceSet(int maxDist, Set<Sequence> seedSet)
	{
		Set<Sequence> resultSet = new HashSet<Sequence>();
		for (int i = 0; i < elements.size(); i++)
		{
			if (seedSet.contains(elements.get(i).getItemset()))
			{
				continue;
			}
			for (int j = i; j < elements.size() && j <= i + maxDist; j++)
			{
				if (seedSet.contains(elements.get(j)))
				{
					continue;
				}
				Sequence seq = new Sequence(elements.get(i).getItemset());
				seq.addElement(elements.get(j).getItemset());
				resultSet.add(seq);
			}
		}
		return resultSet;
	}

	/** extends itemsets with all ancestors of present items, uses normal representation of sequence
	 * @param taxonomy hierarchy of words */
	public void extendSequence(Taxonomy taxonomy)
	{
		if (taxonomy == null)
		{
			return;
		}
		Itemset elem;
		Set<String> ancestors;
		ArrayList<Integer> times;
		for (DbElement dbElement : elements)
		{
			elem = dbElement.getItemset();
			for (String item : elem.getItems())
			{
				boolean foundAncestor = false;
				ancestors = taxonomy.getAncestors(item);
				for (String ancestor : ancestors)
				{
					times = occurrences.get(ancestor);
					if (times == null)
					{
						times = new ArrayList<Integer>();
						occurrences.put(ancestor, times);
					}
					int insertIndex = 0;
					for (int time : times)
					{
						if (time == dbElement.getTime())
						{
							foundAncestor = true;
						}
						if (time < dbElement.getTime())
						{
							insertIndex++;
						}
					}
					if (foundAncestor == false)
					{
						times.add(insertIndex, dbElement.getTime());
					}
				}
				elem.getItems().addAll(ancestors);
			}
		}
	}

	/** extends itemsets with all ancestors of present items, uses vector representation of sequence
	 * @param taxonomy hierarchy of words */
	public void extendSequence2(Taxonomy taxonomy)
	{
		if (taxonomy == null)
		{
			return;
		}
		Set<String> ancestors;
		ArrayList<Integer> times;

		for (Map.Entry<String, ArrayList<Integer>> entry : occurrences.entrySet())
		{
			for (int time : entry.getValue())
			{
				ancestors = taxonomy.getAncestors(entry.getKey());
				for (String ancestor : ancestors)
				{
					boolean foundAncestor = false;
					times = occurrences.get(ancestor);
					if (times == null)
					{
						times = new ArrayList<Integer>();
						occurrences.put(ancestor, times);
					}
					int insertIndex = 0;
					for (int ancestorTime : times)
					{
						if (ancestorTime == time)
						{
							foundAncestor = true;
						}
						if (ancestorTime < time)
						{
							insertIndex++;
						}
					}
					if (foundAncestor == false)
					{
						times.add(insertIndex, time);
					}

				}
			}
		}
	}

	@Override
	public String toString()
	{
		return "size:" + getSize() + elements + ",  occurrences=" + occurrences;
	}

	/** checks if database sequence containce supports sequence with given maxDist param.
	 * @param maxDist maximal elements between elements of sequence */
	public boolean contains(Sequence seq, int maxDist)
	{
		ArrayList<Itemset> seqElems = seq.getElements();
		int[] matchedElements = new int[seqElems.size()];
		return forwardPhase(seqElems, matchedElements, maxDist, 0);
	}


	/** forward phase of algorithm checking support of given sequence by this database sequence */
	private boolean forwardPhase(ArrayList<Itemset> seqElems, int[] matchedTimes, int maxDist, int elemIndex)
	{
		for (int i = elemIndex; i < seqElems.size(); i++)
		{
			Itemset elem = seqElems.get(i);
			Integer elemTime;
			if (i == 0)
			{
				elemTime = getElemSince(elem, Integer.MIN_VALUE, true);
				if (elemTime != null)
				{
					matchedTimes[i] = elemTime;
				}
				else
				{
					return false;
				}
			}
			else
			{
				elemTime = getElemSince(elem, matchedTimes[i - 1], false);
				if (elemTime != null)
				{
					matchedTimes[i] = elemTime;
					if (matchedTimes[i] - matchedTimes[i - 1] > maxDist)
					{
						return backwardPhase(seqElems, matchedTimes, maxDist, i);
					}
				}
				else
				{
					return false;
				}
			}
		}
		return true;
	}

	/** backward phase of algorithm checking support of given sequence by this database sequence */
	private boolean backwardPhase(ArrayList<Itemset> seqElems, int[] matchedTimes, int maxDist, int elemIndex)
	{
		Integer elemTime;
		for (int j = elemIndex; j > 0; j--)
		{
			if (matchedTimes[j] - matchedTimes[j - 1] <= maxDist)
			{
				return forwardPhase(seqElems, matchedTimes, maxDist, j + 1);
			}
			elemTime = getElemSince(seqElems.get(j - 1), matchedTimes[j] - maxDist, true);
			if (elemTime != null)
			{
				matchedTimes[j - 1] = elemTime;
			}
			else
			{// brak takiej sekwencji
				return false;
			}
		}
		return forwardPhase(seqElems, matchedTimes, maxDist, 1);
	}

	/** returns first occurence time of given element since/after given time
	 * @param elem element to search
	 * @param since time after which to search for element
	 * @param inclusive search since or after given time */
	private Integer getElemSince(Itemset elem, int since, boolean inclusive)
	{
		ArrayList<Integer> times;
		int startTime = Integer.MAX_VALUE, endTime = Integer.MIN_VALUE;
		for (String item : elem.getItems())
		{
			boolean found = false;
			times = occurrences.get(item);
			if (times != null && times.isEmpty() == false)
			{
				for(int i = 0 ; i < times.size(); i ++) {
					boolean timeConstraint = (inclusive) ? (times.get(i) >= since) : (times.get(i) > since);
					if (timeConstraint)
					{
						found = true;
						if (times.get(i) > endTime)
						{
							endTime = times.get(i);
						}
						if (times.get(i) < startTime)
						{
							startTime = times.get(i);
						}
						break;

					}
				}
			}
			if (found == false)
			{
				return null;
			}
		}
		if (endTime - startTime <= windowSize)
		{
			return endTime;
		}
		return getElemSince(elem, endTime - windowSize, inclusive);
	}

	@Test
	public void  testGetElemSince() {
		assertEquals(new Integer(1), new Integer(1));
	}
	
}
